package com.ely.coupon.common.core.utils;

import cn.hutool.jwt.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Service
public class JwtService {
    /**
     * 加密盐值
     */
    @Value("${application.security.jwt.secret-key}")
    private String secretKey;

    /**
     * Token失效时间
     */
    @Value("${application.security.jwt.expiration}")
    private long jwtExpiration;

    /**
     * Token刷新时间
     */
    @Value("${application.security.jwt.refresh-token.expiration}")
    private long refreshExpiration;

    /**
     * 从Token中获取Username
     *
     * @param token Token
     * @return String
     */
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    /**
     * 从Token中回去数据,根据传入不同的Function返回不同的数据
     * eg: String extractUsername(String token)
     */
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    /**
     * 生成Token无额外信息
     */
    public String generateToken(UserDetails userDetails) {
        return generateToken(new HashMap<>(), userDetails);
    }

    /**
     * 生成Token,有额外信息
     *
     * @param extraClaims 额外的数据
     * @param userDetails 用户信息
     * @return String
     */
    public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
        return buildToken(extraClaims, userDetails, jwtExpiration);
    }

    /**
     * 生成刷新用的Token
     *
     * @param userDetails 用户信息
     * @return String
     */
    public String generateRefreshToken(UserDetails userDetails) {
        return buildToken(new HashMap<>(), userDetails, refreshExpiration);
    }

    /**
     * 构建Token方法
     *
     * @param extraClaims 额外信息
     * @param userDetails //用户信息
     * @param expiration  //失效时间
     * @return String
     */
    private String buildToken(Map<String, Object> extraClaims, UserDetails userDetails, long expiration) {
        return Jwts.builder().setClaims(extraClaims) //body
                .setSubject(userDetails.getUsername()) //主题数据
                .setIssuedAt(new Date(System.currentTimeMillis())) //设置发布时间
                .setExpiration(new Date(System.currentTimeMillis() + expiration)) //设置过期时间
                .signWith(getSignInKey(), SignatureAlgorithm.HS256) //设置摘要算法
                .compact();
    }

    /**
     * 验证Token是否有效
     *
     * @param token       Token
     * @param userDetails 用户信息
     * @return boolean
     */
    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
    }

    /**
     * 判断Token是否过去
     */
    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    /**
     * 从Token中获取失效时间
     */
    private Date extractExpiration(String token) {
        //通用方法,传入一个Function,返回一个T
        return extractClaim(token, Claims::getExpiration);
    }

    /**
     * 从Token中获取所有数据
     */
    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder().setSigningKey(getSignInKey()).build().parseClaimsJws(token).getBody();
    }

    /**
     * 获取签名Key
     * Token 加密解密使用
     */
    private Key getSignInKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}
3.4
        注册和登录 token的生成已经看过了, 下面该进入最关键的环节了.用户注册 &用户登录

用户注册:接收到用户传递过来的信息,

在数据库中生成用户信息(密码会通过passwordEncoder进行加密).用户信息保存成功后,
会根据用户信息创建一个鉴权token和一个refreshToken 用户登录:获取到用户传递的账号密码后,会创建一个UsernamePasswordAuthenticationToken对象.然后通过authenticationManager的authenticate方法进行校验,如果出现错误会根据错误的不同抛出不同的异常.在实际开发中可以通过捕获的异常类型不同来创建响应提示 . @RestController @RequestMapping("/api/v1/auth") @RequiredArgsConstructor

public class AuthenticationController {

    private final AuthenticationService service;

    /**
     * 注册方法
     *
     * @param request 请求体
     * @return ResponseEntity
     */
    @PostMapping("/register")
    public ResponseEntity<AuthenticationResponse> register(@RequestBody RegisterRequest request) {
        return ResponseEntity.ok(service.register(request));
    }

    /**
     * 鉴权(登录方法)
     *
     * @param request 请求体
     * @return ResponseEntity
     */
    @PostMapping("/authenticate")
    public ResponseEntity<AuthenticationResponse> authenticate(@RequestBody AuthenticationRequest request) {
        return ResponseEntity.ok(service.authenticate(request));
    }

    /**
     * 刷新token
     *
     * @param request  请求体
     * @param response 响应体
     * @throws IOException 异常
     */
    @PostMapping("/refresh-token")
    public void refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
        service.refreshToken(request, response);
    }
}

可以看出来controller中的方法都是对service方法的调用,我们现在看一下service中的代码

@Service
@RequiredArgsConstructor

public class AuthenticationService {

    private final UserRepository repository; //访问user数据库
    private final TokenRepository tokenRepository; //访问token数据库
    private final PasswordEncoder passwordEncoder; //密码加密器
    private final JwtService jwtService; //JWT 相关方法
    private final AuthenticationManager authenticationManager; //Spring Security 认证管理器

    /**
     * 注册方法
     *
     * @param request 请求体
     * @return AuthenticationResponse(自己封装的响应结构)
     */
    public AuthenticationResponse register(RegisterRequest request) {
        //构建用户信息
        var user = User.builder().firstname(request.getFirstname()).lastname(request.getLastname()).email(request.getEmail()).password(passwordEncoder.encode(request.getPassword())).role(request.getRole()).build();

        //将用户信息保存到数据库
        var savedUser = repository.save(user);
        //通过JWT方法生成Token
        var jwtToken = jwtService.generateToken(user);
        //生成RefreshToken(刷新Token使用)
        var refreshToken = jwtService.generateRefreshToken(user);
        //将Token保存到数据库
        saveUserToken(savedUser, jwtToken);
        //返回响应体
        return AuthenticationResponse.builder().accessToken(jwtToken).refreshToken(refreshToken).build();
    }

    /**
     * 鉴权(登录)方法
     *
     * @param request 请求体
     * @return AuthenticationResponse(自己封装的响应结构)
     */
    public AuthenticationResponse authenticate(AuthenticationRequest request) {
        //通过Spring Security 认证管理器进行认证
        //如果认证失败会抛出异常 eg:BadCredentialsException 密码错误 UsernameNotFoundException 用户不存在
        authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()));
        //通过邮箱查询用户信息,当前项目email就是账号
        var user = repository.findByEmail(request.getEmail()).orElseThrow();
        //通过JWT方法生成Token
        var jwtToken = jwtService.generateToken(user);
        //生成RefreshToken(刷新Token使用)
        var refreshToken = jwtService.generateRefreshToken(user);
        //将之前所有的Token变成失效状态
        revokeAllUserTokens(user);
        //保存新的Token到数据库
        saveUserToken(user, jwtToken);
        //封装响应体
        return AuthenticationResponse.builder().accessToken(jwtToken).refreshToken(refreshToken).build();
    }


    /**
     * 刷新token方法
     *
     * @param request  请求体
     * @param response 响应体
     * @throws IOException 抛出IO异常
     */
    public void refreshToken(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //从请求头中获取中获取鉴权信息 AUTHORIZATION
        final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
        final String refreshToken;
        final String userEmail;
        //如果鉴权信息为空或者不是以Bearer 开头的,直接返回
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return;
        }
        //从鉴权信息中获取RefreshToken
        refreshToken = authHeader.substring(7);
        //从RefreshToken中获取用户信息
        userEmail = jwtService.extractUsername(refreshToken);
        if (userEmail != null) {
            //根据用户信息查询用户,如果用户不存在抛出异常
            var user = this.repository.findByEmail(userEmail).orElseThrow();

            //验证Token是否有效
            if (jwtService.isTokenValid(refreshToken, user)) {
                //生成新的Token
                var accessToken = jwtService.generateToken(user);
                revokeAllUserTokens(user);
                saveUserToken(user, accessToken);
                //生成新的Token和RefreshToken并通过响应体返回
                var authResponse = AuthenticationResponse.builder().accessToken(accessToken).refreshToken(refreshToken).build();
                new ObjectMapper().writeValue(response.getOutputStream(), authResponse);
            }
        }
    }
}